// Fireworks JavaScript Command // Install by copying to Fireworks/Configuration/Commands/ // Version 2.2.3 // Aaron Beall 2011 - http://abeall.com // Inspired by Kleanthis Economou(http://projectfireworks.com) Fillet & Curvia commands /* TODO - [DONE! woohoo!] work on curved points: preserve bezier segment shape when rounding off a point with existing bezier handles - validate path input - [DONE] merge nodes that overlap(ie, maximum curve radius used) - [DONE-Sharpen Points] allow some way to remove or change a fillet corner after it's been done - [DONE-v2.1] interpolate more accurate handle - optimize with inline vector math - [DONE-v2.2] copy/paste node rather than generic copy/paste object - [DONE-v2.2] make it work with dynamic node info */ var dom = fw.getDocumentDOM(); // document object var sel = new Array().concat(fw.selection); // saved selection var HANDLE_INTERPOLATION = fw.ellipseBCPConst || 0.55229; function RoundCorners() { // validate selection if(!sel.length) return false; var paths = []; var s = sel.length; while(s--) if(sel[s] == '[object Path]') paths.push(sel[s]); if(!paths.length) return false; // user input var radius = 'undefined' var error = undefined; while(isNaN(Number(radius)) && radius!=null){ if(error)alert(error); radius = prompt('Enter rounded corner radius:', (fw.RoundCorners_radius || 5)); error = 'Please enter a number'; } if(radius == null)return false; radius = Number(radius); fw.RoundCorners_radius = radius; //remember user input for next time // core algorithm - apply to all contours of all selected elements var con, ln, oldNodes, oldSelected, newNodes; var prevNode, nextNode, on, d1, d2, r1, r2, maxr, r, a, t, nn, m, predCurve, succCurve; for(var s = 0; s < paths.length; s++){ for(var c = 0; c < paths[s].contours.length; c++){ con = paths[s].contours[c]; ln = con.nodes.length; oldNodes = []; oldSelected = []; //nodes that will actually apply effect to newNodes = []; // construct copy of old contour nodes, and specify selected/not-selected nodes for(var n = 0 ; n < ln ; n++){ oldNodes[n] = copyNode(con.nodes[n]); // automatically select all nodes if Subselect tool not in use oldSelected[n] = fw.activeTool!='Subselection' ? true : con.nodes[n].isSelectedPoint; } //build new nodes prevNode = undefined; for(var n = 0 ; n < ln ; n++){ //apply effect to selected nodes //(skip first and last node only on open paths) if(oldSelected[n] && !(!con.isClosed && (n == 0 || n == ln - 1))){ //determine preceding and succeeding nodes(account for beginning/end of contour loop around) prevNode = (prevNode==undefined) ? oldNodes[ln - 1] : prevNode; nextNode = (oldNodes[n + 1] == undefined) ? oldNodes[0] : oldNodes[n + 1]; on = oldNodes[n]; // determine if there are curves involved, which requires a completely different algorithm predCurve = isCurve(on) || prevNode.x != prevNode.succX || prevNode.y != prevNode.succY; succCurve = isCurve(on) || nextNode.x != nextNode.predX || nextNode.y != nextNode.predY; // determine maximum distance for straight corners(max distance for curved corners is calculated in the bezier functions) if(!predCurve || !succCurve){ d1 = getDistance(on, prevNode); d2 = getDistance(on, nextNode); r1 = (prevNode.isSelectedPoint || fw.activeTool != 'Subselection') ? d1 / 2 : d1; r2 = (nextNode.isSelectedPoint || fw.activeTool != 'Subselection') ? d2 / 2 : d2; maxr = Math.min(r1,r2); r = Math.min(radius,maxr); } // determine the new curve points -- there is a complex, slower, less accurate solution for curved points, // and a faster more accurate version that only works on straight points // preceding point if(predCurve){ // solution for beziers with curves, preceding rounded corner node prevNode = newNodes[newNodes.length - 1] || oldNodes[ln - 1]; var pt = splitBezierAtDistance({x:prevNode.x, y:prevNode.y}, {x:prevNode.succX, y:prevNode.succY}, {x:on.predX, y:on.predY}, {x:on.x, y:on.y}, radius); //nn = newNodes[newNodes.length-1]||oldNodes[ln-1]; // adjust preceding node's succeeding handle prevNode.succX = pt.cp1.x; prevNode.succY = pt.cp1.y; newNodes.push(copyNode(on)); // new curve node nn = newNodes[newNodes.length - 1]; nn.isCurvePoint = true; nn.x = pt.p3.x; nn.y = pt.p3.y; nn.predX = pt.cp3.x; nn.predY = pt.cp3.y; nn.succX = pt.cp4.x; nn.succY = pt.cp4.y; //newNodes.push(copyNode(on)); // original node - only useful for testing, otherwise don't include it(we turn it into a rounded corner) //nn = newNodes[newNodes.length-1]; //nn.predX = pt.cp2.x; nn.predY = pt.cp2.y; a = getAngle(nn, {x:nn.succX, y:nn.succY}); // adjust handle of new curve to be half as long as the distance between it and the original node d1 = getDistance(nn, on); t = getVector(d1 * HANDLE_INTERPOLATION, a); nn.succX = nn.x + t.x; nn.succY = nn.y + t.y; //alert(nn.x+','+nn.y); }else{ // solution for straight corners, add a preceding node with a succeeding curve handle newNodes.push(copyNode(on)); a = getAngle(oldNodes[n], prevNode); t = getVector(r, a); nn = newNodes[newNodes.length - 1]; nn.x += t.x; nn.y += t.y; nn.predX = nn.x; nn.predY = nn.y; m = interpolate(nn, on, HANDLE_INTERPOLATION); nn.succX = m.x; nn.succY = m.y; //alert(nn.x+','+nn.y); } // succeeding point if(succCurve){ // solution for beziers with curves, succeeding rounded corner node nextNode = oldNodes[n + 1] || newNodes[0]; pt = splitBezierAtDistance({x:nextNode.x, y:nextNode.y}, {x:nextNode.predX, y:nextNode.predY}, {x:on.succX, y:on.succY}, {x:on.x, y:on.y}, radius); on.succX = pt.cp4.x; on.succY = pt.cp4.y; // adjust original node's succeeding handle newNodes.push(copyNode(on)); // new following curve node nn.isCurvePoint = true; nn = newNodes[newNodes.length - 1]; nn.x = pt.p3.x; nn.y = pt.p3.y; nn.predX = pt.cp4.x; nn.predY = pt.cp4.y; nn.succX = pt.cp3.x; nn.succY = pt.cp3.y; nextNode.predX = pt.cp1.x; // adjust succeeding node's preceding handle nextNode.predY = pt.cp1.y; a = getAngle(nn, {x:nn.predX, y:nn.predY}); // adjust handle of new curve to be half as long as the distance between it and the original node d1 = getDistance(nn, on); t = getVector(d1 * HANDLE_INTERPOLATION, a); nn.predX = nn.x + t.x; nn.predY = nn.y + t.y; //alert(nn.x+','+nn.y); }else{ // solution for straight corners, add a succeeding node with a preceeding curve handle newNodes.push(copyNode(on)); a = getAngle(on, nextNode); t = getVector(r, a); nn = newNodes[newNodes.length - 1]; nn.x += t.x; nn.y += t.y; nn.succX = nn.x; nn.succY = nn.y; m = interpolate(nn, on, HANDLE_INTERPOLATION); nn.predX = m.x; nn.predY = m.y; //alert(nn.x+','+nn.y); } // check to see if nodes overlap and can be merged(due to maximum radius being used) if(newNodes.length > 2 && r == maxr){ if(newNodes[newNodes.length - 2].x == newNodes[newNodes.length - 3].x && newNodes[newNodes.length - 2].y == newNodes[newNodes.length - 3].y){ newNodes[newNodes.length - 3].succX = newNodes[newNodes.length - 2].succX; newNodes[newNodes.length - 3].succY = newNodes[newNodes.length - 2].succY; newNodes[newNodes.length - 2] = newNodes.pop(); } } //and now we have taken the selected node and made it into two nodes that make a smooth corner! }else{ //if not selected, add uneffected newNodes.push(copyNode(oldNodes[n])); } prevNode = oldNodes[n]; } //create new contour in place of old contour in the selected element var isClosed = con.isClosed; paths[s].contours[c] = new Contour(); paths[s].contours[c].isClosed = isClosed; //and add all the new nodes for(var n=0 ; ndistance) return --iterations ? doWalkBezier(point.percent,prevPoint.percent,dist-currDist) : prevPoint; } prevPoint = point; } //point = getBezier(minPercent,p1,cp1,cp2,p2); //point.percent = minPercent; return point; } } // find a point along a bezier segment(as defined by p1, cp1, cp2, p2) at a specified percent(0-100) function getBezier(percent,p1,cp1,cp2,p2) { function b1(t) { return t*t*t } function b2(t) { return 3*t*t*(1-t) } function b3(t) { return 3*t*(1-t)*(1-t) } function b4(t) { return (1-t)*(1-t)*(1-t) } var pos = {x:0,y:0}; pos.x = p1.x*b1(percent) + cp1.x*b2(percent) + cp2.x*b3(percent) + p2.x*b4(percent); pos.y = p1.y*b1(percent) + cp1.y*b2(percent) + cp2.y*b3(percent) + p2.y*b4(percent); return pos; } // find the distance between two points function getDistance(p1,p2){ return Math.sqrt(((p1.x-p2.x)*(p1.x-p2.x))+((p1.y-p2.y)*(p1.y-p2.y))); }